package me.itsoo.elasticbatis.builder.xml;

import me.itsoo.elasticbatis.builder.BaseBuilder;
import me.itsoo.elasticbatis.builder.BuilderException;
import me.itsoo.elasticbatis.datasource.DataSourceFactory;
import me.itsoo.elasticbatis.io.Resources;
import me.itsoo.elasticbatis.mapping.Environment;
import me.itsoo.elasticbatis.parsing.XNode;
import me.itsoo.elasticbatis.parsing.XPathParser;
import me.itsoo.elasticbatis.session.Configuration;

import java.io.InputStream;
import java.io.Reader;
import java.util.Properties;

/**
 * XMLConfigBuilder
 *
 * @author zhangxy4
 * @version 0.0.1
 * @since 2025/4/4
 */
public class XMLConfigBuilder extends BaseBuilder {

    private boolean parsed;
    private final XPathParser parser;
    private String environment;

    public XMLConfigBuilder(Reader reader) {
        this(reader, null, null);
    }

    public XMLConfigBuilder(Reader reader, String environment) {
        this(reader, environment, null);
    }

    public XMLConfigBuilder(Reader reader, String environment, Properties props) {
        this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
    }

    public XMLConfigBuilder(InputStream inputStream) {
        this(inputStream, null, null);
    }

    public XMLConfigBuilder(InputStream inputStream, String environment) {
        this(inputStream, environment, null);
    }

    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
        this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    }

    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
        super(new Configuration());

        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
    }

    public Configuration parse() {
        if (parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }

        this.parsed = true;
        this.parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }

    private void parseConfiguration(XNode root) {
        try {
            this.environmentsElement(root.evalNode("environments"));
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }

    private void environmentsElement(XNode context) {
        if (context != null) {
            if (environment == null) {
                environment = context.getStringAttribute("default");
            }
            for (XNode child : context.getChildren()) {
                String id = child.getStringAttribute("id");
                if (isSpecifiedEnvironment(id)) {
                    DataSourceFactory dsFactory = this.dataSourceElement(child.evalNode("dataSource"));
                    configuration.setEnvironment(Environment.builder()
                            .id(id)
                            .dataSource(dsFactory.getDataSource())
                            .build());
                }
            }
        }
    }

    private DataSourceFactory dataSourceElement(XNode context) {
        if (context != null) {
            Properties props = context.getChildrenAsProperties();
            DataSourceFactory factory = new DataSourceFactory();
            factory.setProperties(props);
            return factory;
        }
        throw new BuilderException("Environment declaration requires a DataSourceFactory.");
    }

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode child : parent.getChildren()) {
                if ("package".equals(child.getName())) {
                    String mapperPackage = child.getStringAttribute("name");
                    configuration.addMappers(mapperPackage);
                    continue;
                }

                String resource = child.getStringAttribute("resource");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && mapperClass == null) {
                    InputStream is = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(is, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && mapperClass != null) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }

    private boolean isSpecifiedEnvironment(String id) {
        if (environment == null) {
            throw new BuilderException("No environment specified.");
        } else if (id == null) {
            throw new BuilderException("Environment requires an id attribute.");
        }

        return environment.equals(id);
    }
}
